﻿using System;
using System.Collections.Generic;

namespace LoRel {
    public class Core {
        private readonly Dictionary< String, FieldDeclaration > m_FieldLookup;
        private readonly Dictionary< String, Dictionary<String, ColumnItem > > m_ColumnLookup;
        private readonly Dictionary< String, TableReference > m_TableLookup;
        private readonly Dictionary< String, KeyItem > m_KeyLookup;
        private readonly Dictionary<ProxyKey, Proxy> m_ProxyLookup;
        private readonly Dictionary<ProxyKey, Proxy> m_KeyProxyLookup;
        private readonly Dictionary< ColumnSetProxyKey, Proxy> m_ColumnProxyLookup;

        private class ColumnSetProxyKey {
            private ColumnSet m_ColumnSet;
            private Type m_ExternalType;
            public ColumnSetProxyKey( ColumnSet column_set, Type external_type ) {
                m_ColumnSet = column_set;
                m_ExternalType = external_type;
            }

            public bool Equals( ColumnSetProxyKey other ) {
                if( ReferenceEquals( null, other ) ) return false;
                if( ReferenceEquals( this, other ) ) return true;
                return Equals( other.m_ColumnSet, m_ColumnSet ) && Equals( other.m_ExternalType, m_ExternalType );
            }

            public override bool Equals( object obj ) {
                if( ReferenceEquals( null, obj ) ) return false;
                if( ReferenceEquals( this, obj ) ) return true;
                if( obj.GetType() != typeof( ColumnSetProxyKey ) ) return false;
                return Equals( ( ColumnSetProxyKey ) obj );
            }

            public override int GetHashCode() {
                unchecked {
                    return ( ( m_ColumnSet != null ? m_ColumnSet.GetHashCode() : 0 )*397 ) ^ ( m_ExternalType != null ? m_ExternalType.GetHashCode() : 0 );
                }
            }
        }
        public Core() {
            m_FieldLookup = new Dictionary< string, FieldDeclaration >();
            m_ColumnLookup = new Dictionary< string, Dictionary< string, ColumnItem > >();
            m_TableLookup = new Dictionary< string, TableReference >();
            m_KeyLookup = new Dictionary< string, KeyItem >();
            m_ProxyLookup = new Dictionary< ProxyKey, Proxy >();
            m_KeyProxyLookup = new Dictionary< ProxyKey, Proxy >();
            m_ColumnProxyLookup = new Dictionary<ColumnSetProxyKey, Proxy>();
        }

        public FieldDeclaration DeclareField( String name, Type type ) {

            if( typeof( IComparable ).IsAssignableFrom( type ) == false ) {
                throw new ArgumentException( "Fields must implement IComparable" );
            }

            FieldDeclaration declaration = new FieldDeclaration( name, type );
            m_FieldLookup.Add( name, declaration  );
            return declaration;
        }
        public ColumnDeclaration DeclareColumn( String table, String name ) {
            
            if( m_FieldLookup.ContainsKey( name ) == false ){ throw new ArgumentOutOfRangeException("name", name, "No Field registered for column");}
            ColumnDeclaration declaration = new ColumnDeclaration( table, m_FieldLookup[name] );
            ColumnReference reference = new ColumnReference( declaration );

            Dictionary< String, ColumnItem > table_reference;
            if( m_ColumnLookup.TryGetValue( table, out table_reference ) == false ) {
                table_reference = new Dictionary< string, ColumnItem >();
                m_ColumnLookup.Add( table, table_reference );
            }
            table_reference.Add(name, new ColumnItem(declaration, reference));
            return declaration;
        }

        public KeyDeclaration DeclareKeys( String table, IEnumerable<String> columns ) {
            Dictionary< String, ColumnItem > column_lookup;
            if( m_ColumnLookup.TryGetValue( table, out column_lookup ) == false ) {
                throw new ArgumentOutOfRangeException( "table", String.Format( "No table named '{0}'", table ) );
            }
            List<String> fields = new List< String >( columns );

            ColumnDeclaration[] declarations = new ColumnDeclaration[ fields.Count ];
            ColumnReference[] references = new ColumnReference[ fields.Count ];

            for( int i=0 ; i < fields.Count ; i++ ) {
                ColumnItem column_item;
                if( column_lookup.TryGetValue( fields[i], out column_item ) == false ) {
                    throw new ArgumentOutOfRangeException("columns", String.Format("No Column named '{0}' in table {1}", 
                        fields[i],
                        table));
                    
                }
                declarations[ i ] = column_lookup[ fields[ i ] ].Declaration;
                references[i] = column_lookup[fields[i]].Reference;
            }

            
            KeyDeclaration declaration = new KeyDeclaration( table, declarations );
            // TODO: ColumnSet should have unique representation! In other words, should be keyed properly. 
            KeyReference reference = new KeyReference( table, new ColumnSet( references) );

            KeyItem item = new KeyItem( declaration, reference );
            m_KeyLookup.Add( table, item );

            return declaration;
        }

        public ColumnReference GetColumn( String table, String column ) {
            return m_ColumnLookup[table][column].Reference;
        }
        public KeyReference GetKey( String table ) { return m_KeyLookup[ table ].Reference; }
        public TableReference GetTable( String table ) {
            TableReference table_reference;
            if( m_TableLookup.TryGetValue( table, out table_reference ) == false ) {
                Dictionary< String, ColumnItem > column_lookup = m_ColumnLookup[ table ];
                List<ColumnReference> references = new List< ColumnReference >();
                foreach( ColumnItem item in column_lookup.Values ) references.Add( item.Reference );

                if( m_KeyLookup.ContainsKey( table ) == false ) {
                    throw new InvalidStateException( String.Format( "Table Key must be initialized before requesting the table: '{0}'", table ) );
                }
                // TODO: ColumnSet should be unique and keyed properly!
                table_reference = new TableReference( table, new ColumnSet( references ), m_KeyLookup[table].Reference );
                m_TableLookup.Add( table, table_reference );
            }
            return table_reference;
        }

        public Proxy RegisterProxy( String table, Type datatype ) {
            TableReference reference = GetTable( table );
            Proxy proxy = Proxy.CreateProxy( datatype, reference.Columns );
            ProxyKey key = new ProxyKey(table, datatype );
            m_ProxyLookup.Add( key, proxy );


            Proxy key_proxy = Proxy.CreateProxy( datatype, reference.Keys.Columns );
            m_KeyProxyLookup.Add(key, proxy);

            return proxy;
        }

        public Proxy RegisterColumnSetProxy( Type external_type, ColumnSet set ) {
            Proxy p = Proxy.CreateProxy( external_type, set );
            m_ColumnProxyLookup.Add( new ColumnSetProxyKey( set, external_type ), 
                p);

            return p;
        }

        public Proxy GetProxy( String table, Type datatype ) { return m_ProxyLookup[ new ProxyKey( table, datatype ) ]; }

        public Proxy GetKeyProxy( String table, Type datatype ) { return m_KeyProxyLookup[ new ProxyKey( table, datatype ) ]; }

        public Proxy GetSetProxy( Type external_type, ColumnSet columns ) {
            ColumnSetProxyKey set_proxy = new ColumnSetProxyKey( columns, external_type );
            return m_ColumnProxyLookup[ set_proxy ];
        }

        #region helper objects to associate declarations and references

        private class ColumnItem
        {
            private readonly ColumnDeclaration m_Declaration;
            private readonly ColumnReference m_Reference;
            public ColumnItem(ColumnDeclaration declaration, ColumnReference reference)
            {
                m_Declaration = declaration;
                m_Reference = reference;
            }

            public ColumnDeclaration Declaration { get { return m_Declaration; } }
            public ColumnReference Reference { get { return m_Reference; } }
        }

        public class KeyItem
        {
            private readonly KeyDeclaration m_Declaration;
            private readonly KeyReference m_Reference;

            public KeyItem(KeyDeclaration declaration, KeyReference reference)
            {
                m_Declaration = declaration;
                m_Reference = reference;
            }

            public KeyDeclaration Declaration { get { return m_Declaration; } }
            public KeyReference Reference { get { return m_Reference; } }
        }

        #endregion

        #region proxy key for dictionary

        private class ProxyKey : IComparable
        {
            private readonly String m_Table;
            private readonly Type m_Type;
            private readonly String m_TypeFullName;

            public ProxyKey(string table, Type type)
            {
                m_Table = table;
                m_Type = type;
                m_TypeFullName = type.FullName;
            }

            public bool Equals(ProxyKey other)
            {
                if (ReferenceEquals(null, other)) return false;
                if (ReferenceEquals(this, other)) return true;
                return Equals(other.m_Table, m_Table) && Equals(other.m_Type, m_Type);
            }

            public override bool Equals(object obj)
            {
                if (ReferenceEquals(null, obj)) return false;
                if (ReferenceEquals(this, obj)) return true;
                if (obj.GetType() != typeof(ProxyKey)) return false;
                return Equals((ProxyKey)obj);
            }

            public override int GetHashCode()
            {
                unchecked
                {
                    return ((m_Table != null ? m_Table.GetHashCode() : 0) * 397) ^ (m_Type != null ? m_Type.GetHashCode() : 0);
                }
            }

            public int CompareTo(object obj)
            {
                ProxyKey rhs = (ProxyKey)obj;
                int compare = System.String.CompareOrdinal(m_Table, rhs.m_Table);
                if (compare == 0) return System.String.CompareOrdinal(m_TypeFullName, rhs.m_TypeFullName);
                return compare;
            }
        }

        #endregion 

    }
}